RED Method로 API 모니터링 시작하기
RED Method로 API 모니터링 시작하기
RED Method는 서비스마다 Rate, Errors, Duration을 같은 방식으로 관측해 “얼마나 요청받고, 얼마나 실패하며, 얼마나 오래 걸리는가”를 빠르게 파악하는 출발점이다. 중요한 것은 지표 세 개를 그리는 일이 아니라 요청의 시작과 종료, 업무 실패, timeout을 일관된 이벤트로 정의하는 것이다.
목차
- #서버가 살아 있다는 것만으로는 부족하다
- #RED Method의 세 가지 질문
- #Rate는 단순 접속자 수가 아니다
- #Errors는 HTTP 5xx보다 넓게 정의한다
- #Duration은 분포로 기록한다
- #하나의 완료 이벤트에서 세 지표를 만든다
- #재구성한 Node.js 계측 예제
- #라벨은 조사 가능성과 비용 사이의 계약이다
- #PromQL로 RED 대시보드 구성하기
- #HTTP가 아닌 작업에 RED 적용하기
- #RED와 Golden Signals USE Method의 관계
- #알림은 증상과 사용자 영향에 연결한다
- #배포 직후 장애를 조사하는 흐름
- #도입할 때 자주 생기는 실수
- #마무리
- #참고 자료
- #관련 노트
서버가 살아 있다는 것만으로는 부족하다
서비스를 처음 운영할 때 모니터링은 흔히 CPU, 메모리, 프로세스 생존 여부에서 시작한다. 이 값들은 필요하지만 사용자가 정상적인 응답을 받고 있는지 직접 말해 주지는 않는다.
CPU usage: 32%
Memory usage: 61%
Process: running
이 세 값이 모두 정상이어도 다음 상황은 가능하다.
- 외부 결제 API가 timeout 나서 결제가 끝나지 않는다.
- 데이터베이스 연결 풀을 기다리느라 p99가 8초까지 늘어난다.
- 캐시된 오류 응답을 매우 빠르게 반환한다.
- HTTP 상태는
200이지만 본문에는payment_failed가 들어 있다. - 특정 모바일 앱 버전의 요청만 validation 오류로 실패한다.
반대로 CPU가 순간 90%라고 해서 반드시 사용자 장애인 것도 아니다. 요청률이 증가했지만 오류율과 지연 시간이 목표 범위에 있다면 시스템이 계획대로 자원을 사용하고 있을 수 있다.
그래서 서비스 모니터링은 인프라 내부 상태와 함께 사용자가 보는 요청 결과에서 시작해야 한다. RED Method는 이 출발점을 세 질문으로 고정한다.
RED Method의 세 가지 질문
RED는 다음 세 단어의 첫 글자다.
| 지표 | 질문 | 대표 단위 |
|---|---|---|
| Rate | 단위 시간에 얼마나 많은 작업을 받는가 | requests/s, jobs/min |
| Errors | 그중 얼마나 실패하는가 | error ratio, errors/s |
| Duration | 완료까지 얼마나 걸리는가 | seconds, p50/p95/p99 |
세 지표는 서로 보완한다.
flowchart LR
A[Rate] --> D[서비스 증상]
B[Errors] --> D
C[Duration] --> D
A --- A1[트래픽과 수요 변화]
B --- B1[요청 성공 여부]
C --- C1[사용자가 기다린 시간]예를 들어 오류율만 오르면 새 validation이나 의존성 실패를 의심할 수 있다. 요청률과 지연 시간이 함께 오르면 용량 포화를 의심한다. 요청률은 떨어졌는데 오류율이 오른다면 upstream이 트래픽을 차단했거나 클라이언트가 요청을 포기했을 수 있다.
| Rate | Errors | Duration | 첫 번째 가설 |
|---|---|---|---|
| 증가 | 정상 | 증가 | 용량 부족, queue 증가 |
| 정상 | 증가 | 정상 또는 감소 | 빠른 실패, 배포 회귀 |
| 정상 | 정상 | p99만 증가 | 조건부 느린 경로, 재시도 |
| 감소 | 증가 | 증가 | upstream 장애, timeout |
| 급감 | 0에 가까움 | 낮음 | 요청 자체가 도착하지 않음 |
이 표는 원인을 확정하지 않는다. 다만 조사 시작점을 빠르게 좁힌다.
Rate는 단순 접속자 수가 아니다
Rate는 일정 시간 동안 관측한 요청 또는 작업의 변화율이다.
request rate
= completed requests in window
/ window duration
웹 API에서는 보통 초당 요청 수를 사용한다. 하지만 무엇을 하나의 요청으로 셀지는 시스템 경계에 따라 달라진다.
HTTP API → completed HTTP requests / second
message worker → consumed messages / second
batch pipeline → processed records / minute
WebSocket → received messages / second
file service → transferred bytes / second
동시 사용자 수와 요청률은 다르다. 사용자가 1,000명 접속해 있어도 요청을 보내지 않으면 Rate는 낮고, 사용자 10명이 polling을 과도하게 하면 Rate는 높을 수 있다.
요청률을 관측할 때는 최소한 다음을 구분할 수 있어야 한다.
- service 또는 entry point
- method와 정규화한 route
- 성공·실패를 나눌 상태 등급
- 필요하면 region과 배포 version
다만 모든 쿼리와 사용자 ID를 라벨로 넣어서는 안 된다. 이 문제는 뒤에서 다시 다룬다.
Counter를 사용하면 프로세스 시작 이후 누적 완료 요청 수를 기록할 수 있다.
http_server_requests_total{method="GET",route="/articles/:id",status_class="2xx"} 184210
Prometheus에서는 rate()로 일정 구간의 초당 증가량을 계산한다.
sum(
rate(http_server_requests_total[5m])
)
프로세스가 재시작하면 counter가 0으로 돌아가지만 rate()는 counter reset을 고려한다. 대시보드에서 원시 counter를 그대로 그리면 누적 숫자만 올라가므로 운영 변화가 잘 보이지 않는다.
API 증상 대시보드에서는 완료 시점에 상태와 duration을 함께 기록하면 세 지표의 분모를 맞추기 쉽다. 장시간 작업은 수신 수, 진행 중 수, 완료 수를 별도 지표로 두어 queue와 유실을 구분한다.
Errors는 HTTP 5xx보다 넓게 정의한다
가장 쉬운 오류 정의는 status >= 500이다.
function isServerError(statusCode: number): boolean {
return statusCode >= 500;
}
하지만 사용자 관점의 실패는 상태 코드 하나로 끝나지 않는다.
HTTP/1.1 200 OK
Content-Type: application/json
{
"ok": false,
"code": "PAYMENT_DECLINED"
}
결제 거절은 시스템 장애가 아닐 수 있지만 사용자가 목표를 완료하지 못한 이벤트다. 반면 잘못된 비밀번호나 존재하지 않는 리소스는 정상적으로 처리한 업무 결과일 수 있다. 모든 4xx를 장애 오류에 넣으면 봇 트래픽이나 사용자 입력 실수 때문에 알림이 울린다.
따라서 오류를 한 boolean으로만 두기보다 관점별로 분류한다.
| 분류 | 예시 | 운영 해석 |
|---|---|---|
| transport error | 연결 종료, timeout | 서비스 또는 네트워크 실패 |
| server error | HTTP 5xx |
서버가 요청을 완수하지 못함 |
| dependency error | DB, cache, 외부 API 실패 | 하위 의존성 문제 |
| business failure | 재고 없음, 결제 거절 | 제품 흐름의 실패 결과 |
| client error | 잘못된 입력, 인증 실패 | 예상 여부와 비율을 따로 판단 |
| cancellation | 클라이언트 연결 종료 | 사용자 대기와 서버 작업을 함께 확인 |
metric label은 제한된 값만 사용한다.
request_outcomes_total{
route="/payments",
outcome="failure",
failure_kind="dependency"
}
구체적인 예외 메시지나 결제사 응답 문구는 label이 아니라 로그와 trace attribute에 기록한다.
type FailureKind =
| "none"
| "client"
| "server"
| "dependency"
| "timeout"
| "cancelled"
| "business";
function classifyFailure(input: {
statusCode: number;
timedOut: boolean;
cancelled: boolean;
dependencyFailed: boolean;
businessCode?: string;
}): FailureKind {
if (input.timedOut) return "timeout";
if (input.cancelled) return "cancelled";
if (input.dependencyFailed) return "dependency";
if (input.statusCode >= 500) return "server";
if (input.businessCode) return "business";
if (input.statusCode >= 400) return "client";
return "none";
}
errors / requests에서 errors와 requests는 같은 경계와 같은 기간의 이벤트여야 한다. edge에서 센 전체 요청을 애플리케이션 인스턴스의 오류 수와 나누면 누락된 요청 때문에 비율이 왜곡된다.
Duration은 분포로 기록한다
Duration은 요청을 처리하는 데 걸린 시간이다. 사용자가 기다린 시간을 보고 싶다면 서버 핸들러 내부 실행 시간만 재기보다 실제 경계에 가깝게 측정해야 한다.
sequenceDiagram
participant C as Client
participant G as Gateway
participant A as API
participant D as Dependency
C->>G: request
G->>A: forwarded request
A->>D: dependency call
D-->>A: response
A-->>G: API response
G-->>C: responseAPI 내부 지표는 A에서 보낸 시간만 알 수 있다. gateway queue, TLS, CDN, 클라이언트 네트워크 시간을 포함한 사용자 경험은 edge 또는 클라이언트 지표와 차이가 난다. 어느 경계를 재는지 metric 이름과 문서에 써야 한다.
Duration은 평균 하나보다 histogram으로 분포를 기록한다.
http_server_request_duration_seconds_bucket
http_server_request_duration_seconds_sum
http_server_request_duration_seconds_count
p50, p95, p99와 히스토그램 버킷 설계에 관한 자세한 내용은 운영 지표에서 평균보다 백분위수를 보는 이유에서 다뤘다.
성공과 오류 latency를 분리해서 볼 수 있어야 한다. 장애가 발생해 모든 요청이 즉시 500을 반환하면 전체 평균 latency는 오히려 좋아질 수 있기 때문이다.
histogram_quantile(
0.95,
sum by (le, outcome) (
rate(http_server_request_duration_seconds_bucket[5m])
)
)
하나의 완료 이벤트에서 세 지표를 만든다
Rate, Errors, Duration을 서로 다른 코드 경로에서 기록하면 분모가 맞지 않기 쉽다.
request counter → middleware A
error counter → exception handler B
duration → access log hook C
예외 핸들러를 통하지 않은 timeout이나 client disconnect가 빠지면 오류 수는 적고 duration count는 많아진다. 가능한 한 하나의 완료 이벤트에서 세 지표를 함께 갱신한다.
stateDiagram-v2
[*] --> Received
Received --> Completed: response finish
Received --> Failed: exception
Received --> TimedOut: deadline exceeded
Received --> Cancelled: client disconnect
Completed --> Observed
Failed --> Observed
TimedOut --> Observed
Cancelled --> Observed
Observed --> [*]관측 이벤트에 필요한 최소 필드를 구조화하면 계측 구현과 테스트가 쉬워진다.
interface RequestObservation {
method: string;
route: string;
statusClass: "2xx" | "3xx" | "4xx" | "5xx" | "unknown";
outcome: "success" | "failure";
failureKind:
| "none"
| "client"
| "server"
| "dependency"
| "timeout"
| "cancelled"
| "business";
durationSeconds: number;
}
Counter와 histogram은 같은 observation에서 업데이트한다.
function recordObservation(observation: RequestObservation): void {
requestsTotal.inc({
method: observation.method,
route: observation.route,
status_class: observation.statusClass,
outcome: observation.outcome,
failure_kind: observation.failureKind,
});
requestDurationSeconds.observe(
{
method: observation.method,
route: observation.route,
outcome: observation.outcome,
},
observation.durationSeconds,
);
}
재구성한 Node.js 계측 예제
다음 코드는 특정 저장소에서 가져온 것이 아니라 Express와 유사한 서버를 가정해 재구성한 예시다. 실제 프레임워크에서는 이미 검증된 OpenTelemetry 또는 Prometheus middleware를 우선 검토한다.
import { Counter, Histogram } from "prom-client";
const requestsTotal = new Counter({
name: "http_server_requests_total",
help: "Completed HTTP server requests",
labelNames: [
"method",
"route",
"status_class",
"outcome",
"failure_kind",
] as const,
});
const requestDurationSeconds = new Histogram({
name: "http_server_request_duration_seconds",
help: "HTTP server request duration in seconds",
labelNames: ["method", "route", "outcome"] as const,
buckets: [0.025, 0.05, 0.1, 0.2, 0.3, 0.5, 1, 2, 5],
});
요청별 mutable state를 심고 종료 시 정확히 한 번만 기록한다.
type MonitoringState = {
startedAt: bigint;
recorded: boolean;
failureKind: RequestObservation["failureKind"];
};
function elapsedSeconds(startedAt: bigint): number {
return Number(process.hrtime.bigint() - startedAt) / 1_000_000_000;
}
function normalizeRoute(pathPattern: string | undefined): string {
return pathPattern ?? "unmatched";
}
function toStatusClass(statusCode: number | undefined):
RequestObservation["statusClass"] {
if (!statusCode) return "unknown";
if (statusCode >= 200 && statusCode < 300) return "2xx";
if (statusCode >= 300 && statusCode < 400) return "3xx";
if (statusCode >= 400 && statusCode < 500) return "4xx";
if (statusCode >= 500 && statusCode < 600) return "5xx";
return "unknown";
}
완료 함수는 finish, close, 예외 처리 경로에서 여러 번 호출될 수 있으므로 idempotent하게 만든다.
function finishObservation(input: {
state: MonitoringState;
method: string;
routePattern?: string;
statusCode?: number;
}): void {
if (input.state.recorded) return;
input.state.recorded = true;
const statusClass = toStatusClass(input.statusCode);
const failureKind =
input.state.failureKind !== "none"
? input.state.failureKind
: statusClass === "5xx"
? "server"
: statusClass === "4xx"
? "client"
: "none";
recordObservation({
method: input.method,
route: normalizeRoute(input.routePattern),
statusClass,
outcome: failureKind === "none" ? "success" : "failure",
failureKind,
durationSeconds: elapsedSeconds(input.state.startedAt),
});
}
업무 오류가 HTTP 200으로 표현되는 기존 API라면 handler가 상태에 failure kind를 명시한다.
async function createPayment(
request: { monitoring: MonitoringState; body: unknown },
response: { status(code: number): void; json(body: unknown): void },
) {
const result = await paymentService.create(request.body);
if (result.kind === "declined") {
request.monitoring.failureKind = "business";
response.status(200);
response.json({
ok: false,
code: "PAYMENT_DECLINED",
});
return;
}
response.status(201);
response.json({
ok: true,
paymentId: result.id,
});
}
가능하다면 업무 실패도 상태 코드와 API 계약을 더 명확히 설계하는 편이 좋다. 하지만 기존 계약을 즉시 바꿀 수 없다면 관측 계층에서 사용자 결과를 놓치지 않아야 한다.
계측 코드는 성공 경로뿐 아니라 예외와 연결 종료를 테스트한다.
import { describe, expect, it, vi } from "vitest";
describe("request observation", () => {
it("하나의 요청을 중복 기록하지 않는다", () => {
const state: MonitoringState = {
startedAt: process.hrtime.bigint(),
recorded: false,
failureKind: "none",
};
finishObservation({
state,
method: "GET",
routePattern: "/articles/:id",
statusCode: 200,
});
finishObservation({
state,
method: "GET",
routePattern: "/articles/:id",
statusCode: 200,
});
expect(state.recorded).toBe(true);
});
});
실제 테스트에서는 registry를 격리한 뒤 counter 증가량, histogram count, label 값을 검증한다.
라벨은 조사 가능성과 비용 사이의 계약이다
라벨이 많으면 대시보드에서 원하는 조건으로 나누기 쉽다. 그러나 값의 종류가 큰 라벨은 시계열 수와 저장 비용을 폭발시킨다.
series
≈ metric names
× methods
× routes
× status classes
× regions
× versions
× instances
다음 값은 metric label에 넣지 않는 것이 일반적이다.
- user ID와 organization ID
- request ID와 trace ID
- 이메일, IP 주소 같은 개인 정보
- 실제 URL과 쿼리 문자열
- 예외 메시지와 stack trace
- 무제한 외부 API 오류 코드
이런 값은 구조화 로그와 trace에서 찾는다. metric에는 제한된 failure_kind="timeout"을 남기고, 같은 요청의 trace에 구체적인 dependency와 exception을 남기는 식이다.
{
"event": "request_failed",
"route": "/payments",
"failure_kind": "dependency",
"dependency": "payment-provider",
"trace_id": "example-trace-id",
"elapsed_ms": 1842
}
trace_id는 로그 한 건에는 적합하지만 metric label에는 부적합하다. 분산 추적에서 Trace ID와 Span ID 역할에서 metric, log, trace를 연결하는 방법을 더 자세히 다룬다.
라벨을 추가할 때는 세 질문을 한다.
- 이 차이에 따라 운영 행동이 달라지는가
- 값의 최대 종류를 예측할 수 있는가
- metric이 아니라 log나 trace로 답할 수 없는가
PromQL로 RED 대시보드 구성하기
가장 먼저 전체 서비스의 요청률을 그린다.
sum(
rate(http_server_requests_total[5m])
)
route별로 나누되 상위 항목만 보여 대시보드 가독성을 지킨다.
topk(
10,
sum by (route) (
rate(http_server_requests_total[5m])
)
)
사용자 영향 오류율은 정의한 outcome을 기준으로 계산한다.
sum(
rate(http_server_requests_total{outcome="failure"}[5m])
)
/
sum(
rate(http_server_requests_total[5m])
)
실패 종류별 Rate를 보면 원인 후보를 좁힐 수 있다.
sum by (failure_kind) (
rate(
http_server_requests_total{
outcome="failure"
}[5m]
)
)
전체 p50, p95, p99는 동일한 histogram에서 계산한다.
histogram_quantile(
0.50,
sum by (le) (
rate(http_server_request_duration_seconds_bucket[5m])
)
)
histogram_quantile(
0.95,
sum by (le) (
rate(http_server_request_duration_seconds_bucket[5m])
)
)
histogram_quantile(
0.99,
sum by (le) (
rate(http_server_request_duration_seconds_bucket[5m])
)
)
route별 p95는 다음처럼 route와 le를 남긴다.
histogram_quantile(
0.95,
sum by (route, le) (
rate(http_server_request_duration_seconds_bucket[5m])
)
)
대시보드의 기본 단위는 맞춰야 한다. metric 이름이 _seconds인데 패널에서 millisecond로 표시하려면 시각화 도구가 단위를 변환하도록 설정하거나 쿼리 결과에 1,000을 곱한다. 서로 다른 패널이 초와 밀리초를 섞으면 장애 중 오판하기 쉽다.
첫 행에는 Rate와 Errors, 둘째 행에는 p50·p95·p99, 셋째 행에는 route와 failure kind별 상세 패널을 둔다. 아래에 배포 시점 annotation과 dependency RED를 이어 붙이면 위에서 아래로 조사할 수 있다.
HTTP가 아닌 작업에 RED 적용하기
RED는 HTTP 전용 규칙이 아니다. “요청과 응답”에 해당하는 작업 경계를 정의할 수 있으면 비동기 시스템에도 적용할 수 있다.
메시지 worker에서는 다음처럼 바꾼다.
| RED | 메시지 처리 지표 |
|---|---|
| Rate | 초당 처리 완료 메시지 수 |
| Errors | retry 또는 dead-letter로 간 메시지 비율 |
| Duration | 수신 후 처리 완료까지 걸린 시간 |
여기서 handler 실행 시간만 재면 queue에서 기다린 시간을 놓친다.
end-to-end duration
= processing_completed_at
- message_enqueued_at
handler duration
= processing_completed_at
- processing_started_at
둘 다 필요할 수 있다. end-to-end는 사용자나 upstream이 체감한 시간을, handler duration은 worker 코드 실행 시간을 보여 준다.
function observeMessage(input: {
enqueuedAtMs: number;
startedAtMs: number;
completedAtMs: number;
outcome: "success" | "retry" | "dead_letter";
messageType: "email" | "thumbnail" | "webhook";
}) {
messageProcessingTotal.inc({
type: input.messageType,
outcome: input.outcome,
});
messageHandlerDurationSeconds.observe(
{ type: input.messageType, outcome: input.outcome },
(input.completedAtMs - input.startedAtMs) / 1_000,
);
messageEndToEndDurationSeconds.observe(
{ type: input.messageType, outcome: input.outcome },
(input.completedAtMs - input.enqueuedAtMs) / 1_000,
);
}
재시도를 Rate에서 어떻게 셀지도 문서화한다. 처리 시도 횟수와 고유 메시지 완료 수를 따로 두면 retry storm을 발견할 수 있다.
message_processing_attempts_total
message_completed_total
message_retries_total
message_dead_letter_total
RED와 Golden Signals USE Method의 관계
RED 하나로 모든 모니터링이 끝나는 것은 아니다.
Google SRE의 Four Golden Signals는 latency, traffic, errors, saturation을 본다. RED의 Duration, Rate, Errors에 saturation을 더한 형태와 가깝다. saturation은 CPU 사용률 하나가 아니라 서비스가 감당할 수 있는 한계에 얼마나 가까운지를 나타낸다.
thread pool active / max
database connection pool waiters
queue depth and oldest message age
CPU throttling time
remaining disk capacity
USE Method는 자원마다 Utilization, Saturation, Errors를 확인하는 접근이다.
| 방법 | 중심 대상 | 질문 |
|---|---|---|
| RED | 요청을 처리하는 서비스 | 사용자가 어떤 증상을 겪는가 |
| Golden Signals | 사용자 서비스 전반 | latency, traffic, errors, saturation은 어떤가 |
| USE | CPU, disk, pool 같은 자원 | 사용량, 포화, 자원 오류는 어떤가 |
장애 조사에서는 RED로 사용자 증상을 찾고, dependency RED와 USE 지표로 원인을 좁힐 수 있다.
flowchart TD
A[사용자 SLO 또는 RED 이상] --> B[어느 서비스·route인가]
B --> C[하위 서비스 RED 확인]
C --> D[문제 자원의 USE 확인]
D --> E[로그와 trace로 개별 원인 확인]증상 지표 없이 CPU부터 보면 사용자 영향이 없는 노이즈에 시간을 쓸 수 있다. 반대로 RED만 보면 “느리다”는 사실은 알지만 connection pool 포화나 disk 오류 같은 원인을 알기 어렵다.
알림은 증상과 사용자 영향에 연결한다
RED 패널을 그대로 모두 paging alert로 만들면 알람 피로가 생긴다.
request rate changed
error count > 0
p99 > 1s
이 조건들은 대시보드에서는 유용하지만 즉시 사람을 깨울 조건으로는 부족하다.
- 마케팅 행사로 Rate가 증가해도 정상일 수 있다.
- 잘못된 요청 한 건 때문에 error count가 1일 수 있다.
- 트래픽이 10건일 때 p99는 한 표본에 좌우된다.
- batch route의 정상 p99가 1초를 넘을 수 있다.
Paging alert는 사용자가 실제로 약속받은 SLO 위반 속도와 연결하는 편이 좋다.
good event
= successful response
AND duration <= 300ms
SLI
= good events / valid events
PromQL 개념 예시는 다음과 같다.
sum(
rate(
http_server_request_duration_seconds_bucket{
le="0.3",
outcome="success"
}[5m]
)
)
/
sum(
rate(http_server_request_duration_seconds_count[5m])
)
실제 SLO 계산에서는 유효하지 않은 클라이언트 요청, health check, 내부 트래픽을 분모에서 어떻게 제외할지 명확히 해야 한다.
대시보드 경고와 paging을 분리한다.
| 신호 | 일반적인 용도 |
|---|---|
| Rate 급변 | 배포 확인, capacity 조사 |
| failure kind 증가 | 원인 분류용 대시보드 |
| 짧은 p99 spike | 조사·성능 추세 |
| 지속적 SLO burn | paging 후보 |
| queue saturation 임박 | 장애 예방 paging 후보 |
알림 임곗값과 지속 시간 설계는 알람 피로를 줄이는 임계값 설계에서 더 자세히 이어간다.
배포 직후 장애를 조사하는 흐름
배포 후 “API가 느리다”는 제보가 들어왔다고 가정하자. RED 대시보드에서 다음 순서로 본다.
1. Rate가 바뀌었는가
sum by (version) (
rate(http_server_requests_total[5m])
)
신버전이 예상한 트래픽을 받고 있는지 확인한다. Rate가 0이면 애플리케이션이 빠른 것이 아니라 라우팅이나 health check가 실패했을 수 있다.
2. Errors가 어떤 종류로 늘었는가
sum by (version, failure_kind) (
rate(
http_server_requests_total{
outcome="failure"
}[5m]
)
)
dependency가 늘었다면 하위 서비스와 timeout을 본다. client만 늘었다면 API 계약이나 클라이언트 버전 호환성을 확인한다.
3. Duration 분포가 어디에서 갈라지는가
histogram_quantile(
0.99,
sum by (version, route, le) (
rate(http_server_request_duration_seconds_bucket[5m])
)
)
p50은 같고 p99만 신버전에서 상승한다면 특정 입력, 캐시 미스, retry 경로를 찾는다.
4. 하위 의존성으로 내려간다
histogram_quantile(
0.95,
sum by (dependency, version, le) (
rate(outbound_request_duration_seconds_bucket[5m])
)
)
외부 API duration과 error가 함께 올랐는지, connection pool saturation이 있는지 확인한다.
5. trace와 로그로 개별 요청을 확인한다
p99 구간의 exemplar 또는 trace를 열어 어느 span이 critical path를 차지했는지 본다. metric이 “어디가 비정상인가”를 알려 주고 trace와 로그가 “왜 이 요청이 실패했는가”를 설명한다.
이 흐름이 runbook에 들어 있으면 처음 보는 서비스도 같은 방식으로 진단할 수 있다. runbook 구조는 장애 대응 Runbook에 들어가야 할 내용에서 다룬다.
도입할 때 자주 생기는 실수
처리 중인 요청이 유실된다
완료 이벤트에서만 세면 프로세스가 강제 종료된 요청은 기록되지 않을 수 있다. gateway 지표와 in-flight gauge, timeout 지표를 함께 비교한다.
http_server_in_flight_requests
http_server_requests_total
gateway_requests_total
성공 latency만 보고 느린 오류를 버린다
오류를 histogram에서 제외하면 timeout과 느린 실패가 사라진다. 성공과 실패를 함께 기록하되 label로 나눠 본다.
모든 4xx를 장애로 센다
사용자 입력 오류와 인증 공격이 서비스 오류율을 지배할 수 있다. 예상 가능한 client outcome과 시스템 실패를 구분한다. 동시에 특정 4xx 급증이 제품 장애 신호일 수도 있으므로 버리지 말고 별도 분류한다.
route 대신 raw path를 쓴다
/users/1
/users/2
/users/3
이 값을 그대로 label에 넣으면 사용자 수만큼 시계열이 생긴다. /users/:id로 정규화한다.
p95 값을 인스턴스끼리 평균 낸다
summary가 노출한 quantile의 평균은 전체 분포의 quantile이 아니다. 집계가 필요하면 호환되는 histogram bucket을 합친 뒤 계산한다.
관측 시스템 장애가 요청을 실패시킨다
metric push나 trace export가 느리다고 사용자 요청을 기다리게 해서는 안 된다. 로컬 집계와 비동기 export, bounded queue를 사용하고 관측 데이터 드롭 자체를 별도 지표로 확인한다.
telemetry_export_failures_total
telemetry_dropped_spans_total
telemetry_queue_size
대시보드는 있는데 소유자가 없다
metric과 alert에는 서비스 소유 팀, runbook, SLO, 배포 대시보드 링크가 연결되어야 한다. 아무도 행동하지 않는 그래프는 시간이 지나면 신뢰를 잃는다.
오류 정의와 작업 경계는 서비스 도메인에 따라 다르다. 결제 승인, 이미지 변환, 검색 응답에 같은 성공 조건을 기계적으로 적용할 수 없다.
마무리
RED Method의 장점은 복잡한 시스템을 세 지표로 완전히 설명하는 데 있지 않다. 모든 서비스가 같은 세 질문에서 조사를 시작하게 만드는 데 있다.
Rate는 수요, Errors는 완료 결과, Duration은 대기 경험을 보여 준다. 세 값이 같은 요청 완료 이벤트와 같은 경계에서 만들어질 때 서로 비교할 수 있다.
도입 순서는 다음처럼 잡을 수 있다.
- 사용자 관점의 요청 또는 작업 경계를 정한다.
- 성공, 업무 실패, 시스템 실패, timeout을 정의한다.
- 완료 이벤트에서 counter와 duration histogram을 함께 기록한다.
- route와 failure kind를 제한된 라벨로 만든다.
- Rate, error ratio, p50·p95·p99 대시보드를 구성한다.
- 표본 수와 배포 version을 함께 볼 수 있게 한다.
- SLO와 burn rate를 paging alert에 연결한다.
- dependency RED, resource USE, trace와 log로 조사 경로를 확장한다.
처음부터 모든 인프라 지표를 모으려 하면 무엇을 봐야 할지 모르게 된다. 사용자가 요청을 보내고 결과를 받는 가장 바깥 경계에서 RED를 만들고, 실제 장애를 겪으며 필요한 상세 지표를 아래로 추가하는 편이 유지하기 쉽다.
참고 자료
- Grafana Labs - The RED Method: How to Instrument Your Services
- Google SRE Book - Monitoring Distributed Systems
- Google SRE Book - Service Level Objectives
- Prometheus - Histograms and summaries